I've dealt with this in a few different packages including gevent (needs greenlet's headers to build, uses Cython) and most recently, zope.container (needs persistent and zope.proxy to build, pure C). Both are installed in a wide variety of environments using a variety of different tools of different versions (including plain pip and zc.buildout).
To make this work well, it's helpful if the dependencies do two things: install their headers using *both* the `headers=` setup() argument *and* as package data.
Then, in the project that needs them, you need to arrange for the dependencies to be installed at build time. You can use PEP 518's support to declare these dependencies in pyproject.toml's `[build-system]requires` key, but widest compatibility (e.g., with zc.buildout) still requires declaring them in the `setup_requires=` setup() argument. Or you can just document that for the user, but pip's build-time isolation that goes along with pyproject.toml makes that more challenging these days.
Finally, when setup.py runs, you need to discover the location of these headers and add them to the `Extension`. This has to be carefully deferred until setup() is actually running and not before. The easiest way I've seen to do this is to pass a custom object to the `include_dirs` argument of `Extension` In zope.container and a few other projects, this is done by using an object that either does this lookup when its __str__ is called (as in BTrees, which is older), or, in zope.container, acts as a proxy.