TL;DR:
If I spawn 10 web requests, each on its own thread, with a CPU that has a 4 thread limit, is this okay or inefficient? Threads are IO bound so sit idle while awaiting server response (I believe). How does CPU deal if more than 4 threads return simultaneously?
I've got a script that currently starts a new thread for every file I need to download (each located at a unique URL) through an http.client.HTTPSConnection. At max, I may need to spawn 730 threads. I have done this, since the threads are all IO bound work (downloading and saving to file), but I am not sure if they are executing in parallel or if the CPU is only executing a set at a time. Total run time for file sizes ranging between 20MB to 110MB was roughly 15 minutes.
My CPU is quad-core with no Hyper-Threading. This means that it should only support 4 threads simultaneously at any given time. Since the work is IO bound and not CPU bound, am I still limited by the hold of only 4 simultaneous threads?
I suppose what is confusing is I am not sure what sequence of events takes place if say I send out just 1 request on 10 threads; what happens if they all return at the same time? Or how does the CPU choose which 4 to finish before moving onto the next available thread?
And after all of this, if the CPU is only handling 4 threads at a time, I would image it is still smart to spawn as many IO threads as I need (since they will sit idle while waiting for server response) right?
You can have significantly more than 4 IO-bound threads on a quad-core CPU.  However, you  do want to have some maximum.  Even IO bound processes use the CPU some of the time.  For example, when a packet is received, that packet needs to be handled to update TCP state.  If you are reading from a socket and writing to a file, some CPU is required to actually copy the characters from the socket buffer to the file buffer under most circumstances.  If you use TLS, CPU is typically required to decrypt and encrypt data.  So even threads that are mostly doing IO use the CPU some.  Eventually the small fraction of time that you are using the CPU will add up and consume the available CPU resources.
Also, note that in Python, because of the global interpreter lock, you can only have one thread using the CPU to run python code at a time.  So, the GIL would not typically be held while doing something like waiting for an outgoing connection.  During that time other threads could be run.  However, for some fraction of the time while reading and writing from a socket or file, the GIL will be held.  It's likely with most common work loads that the performance of your application will reach a maximum when  the fraction of time your threads need a CPU reaches one full CPU rather than four full CPUs.
You may find that using asyncio or some other event-driven architecture provides better performance.  When true this is typically because the event-driven model is better at reducing cross-thread contention for resources.
In response to your question edit, I would not expect 10 threads to be a problem
Am I overspawning IO bound web scraping threads?